using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
using System.ComponentModel;
using System.Security;
using System.Security.AccessControl;


namespace MonsterNet {
	
	public class MNEventArgs : EventArgs {
		private string _message;
		public string Message { 	
			get { return _message; }
			set { _message = value; }
		}
		
		private bool _complete;
		public bool Complete {
			get { return this._complete; }
			set { _complete = value; }
		}
		
		private string _longMessage;		
		public string LongMessage {
			get {
				return this._longMessage;
			}
			set {
				_longMessage = value;
			}
		}
		
		private int _number;		
		public int Number {
			get {
				return this._number;
			}
			set {
				_number = value;
			}
		}
		
		public MNEventArgs(string message) {
			_message = message;
			_complete = false;
			_longMessage = string.Empty;
		}
		
		public MNEventArgs(string message, int number) {
			_message = message;
			_complete = false;
			_number = number;
		}
		
		public MNEventArgs(bool complete, string message, string longMessage) {
			_complete = complete;
			_message = message;
			_longMessage = longMessage;	
		}
		
		public MNEventArgs(bool complete, string message, uint number) {
			_complete = complete;
			_message = message;
			_longMessage = string.Empty;	
			_number = (int)number;
		}		
	}
	
    public class MNController {
		private const int kScanLevelToDisplay = 2;
		private const bool kCompressPersist = true;
					
		private MNLocation _location;
		
		private MNPersisted _data;
		
		private DateTime _scanningDateTime;
		private bool _hasChanged = false;
		
		private MNQueue _queue;
				
		public delegate void ScanningEvent(MNEventArgs e);
		public event ScanningEvent RaiseScanningEvent;	
		
		public delegate void CopyEvent(MNEventArgs e);
		public event CopyEvent RaiseCopyEvent;
		
		public delegate void SaveLoadEvent(MNEventArgs e);
		public event SaveLoadEvent RaiseSaveLoadEvent;
		
		private bool _cancelAction = false;
		private string _savedPortablePath; // used for saving, if the locations portable path changes, we need this to save to the old path.
		
		private string _username = string.Empty;
		
		#region Properties
		
        public MNSynchronisedDirectory RootDirectory {
            get { return _data.Root; }
        }
		
        public List<string> ExtensionsToIgnore {
            get { return _data.ExtensionsToIgnore; }
            set { _data.ExtensionsToIgnore = value; }
        }
		
		public MNFSObjectLocation CurrentLocation {
			get {
				return _location.CurrentLocation;
			}
		}
		
		public MNQueue Queue {
			get { return _queue; }
			set { _queue = value; }
		}
		
		public string SourcePath
		{
			get {	return _location.Path; }
		}
		
		public string PortablePath
		{
			get {	return _location.PortablePath; }
		}
		
		public bool HasChanged {
			get {
				return _hasChanged;
			}
			set {
				_hasChanged = value;
			}
		}
		
		public bool CancelAction {
			get {
				return this._cancelAction;
			}
			set {
				_cancelAction = value;
			}
		}
		
		public DateTime LastScan {
			get { return _data.LastScanForLocation(_location.CurrentLocation); }
		}
		
		
		public string Username {
			get { return _username;
			}
		}
		#endregion

        #region Constructors, Save/Load
		
        public MNController ()
        {
        	_data = new MNPersisted ();
   
			_hasChanged = false;
        	_cancelAction = false;
   
			_savedPortablePath = string.Empty;
			
			_location = new MNLocation ();
			
			_queue = new MNQueue();
			
			_username = Environment.UserDomainName + "\\" + Environment.UserName;
        }
		
		/// <summary>
		///	setup the controller with a location's settings
		/// load from the portable path the current contents.xml
		/// </summary>
		/// <param name="location">
		/// A <see cref="MNLocation"/>
		/// </param>
		public MNController (MNLocation location, SaveLoadEvent eventHandler) : this()
		{
			SubscribeSaveLoad(eventHandler);
			
			_location = location;
			_savedPortablePath = _location.PortablePath;
			_hasChanged = false;
		
			_username = Environment.UserDomainName + "\\" + Environment.UserName;
			
			LoadPersistantData();
		}
		
		private void LoadPersistantData() {
			//DoRaiseSaveLoadEvent(@"Loading");
			if(Directory.Exists(_location.PortablePath)) {
				//load the persistant data
				string pathname = MNGlobal.CombinePathAndFilename(_location.PortablePath, MNGlobal.kDefaultContentsFileName);
				//TODO: if load fails, ask to load from latest backup file.
		
				_data = MNSerialise.LoadFromFile<MNPersisted>(pathname, kCompressPersist);
				_data.SetUpEvents();
			} else {
				_data = new MNPersisted();
			}			
		}
		
		public void Save()
		{
			if(_hasChanged)
			{
				string pathname = MNGlobal.CombinePathAndFilename(_savedPortablePath, MNGlobal.kDefaultContentsFileName);
				DoRaiseSaveLoadEvent(@"Saving");
				MNSerialise.SaveToFile<MNPersisted>(pathname, _data, kCompressPersist);
				_location.BackupContentsFile(pathname);
			}
		}
		
		#endregion
			
		/// <summary>
		/// Save and reload the controller with new location info. 
		/// </summary>
		/// <param name="newLocation">
		/// A <see cref="MNLocation"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Boolean"/>, true if portable path changed.
		/// </returns>
		/// 
		public bool LocationUpdated (bool reload)
		{
			bool result = false;
			
			//save contents to current path if portable path has changed.
			if (_savedPortablePath != _location.PortablePath) {
				Save ();
				result = true;
			}
			
			if (_savedPortablePath != _location.PortablePath | reload) {
				//reload Contents
				LoadPersistantData ();
				_savedPortablePath = string.Copy(_location.PortablePath);
			}
			
			return result;			
		}

		#region Events
		
		public void SubscribeScanning(ScanningEvent eventHandler) {
			RaiseScanningEvent += eventHandler;
		}
		
		public void UnsubscribeScanning(ScanningEvent eventHandler) {
			RaiseScanningEvent -= eventHandler;
		}
		
		public void SubscribeCopying(CopyEvent eventHandler) {
			RaiseCopyEvent += eventHandler;
		}
		
		public void UnsubscribeCopying(CopyEvent eventHandler) {
			RaiseCopyEvent -= eventHandler;
		}	
		
		public void SubscribeSaveLoad(SaveLoadEvent eventHandler) {
			RaiseSaveLoadEvent += eventHandler;
		}
		
		public void UnsubscribeSaveLoad(SaveLoadEvent eventHandler) {
			RaiseSaveLoadEvent -= eventHandler;
		}
		
		private void DoRaiseScanningEvent(MNEventArgs e) {
			if(RaiseScanningEvent != null)
				RaiseScanningEvent(e);
		}
		
		private void DoRaiseScanningEvent(string directoryName) {			
			MNEventArgs e = new MNEventArgs(directoryName);
			DoRaiseScanningEvent(e);	
		}
		
		private void DoRaiseScanningCompleteEvent(string message) {
			MNEventArgs e = new MNEventArgs(true, message, string.Empty);
			DoRaiseScanningEvent(e);				
		}
		
		private void DoRaiseScanningCompleteEvent(string message, string longMessage) {
			MNEventArgs e = new MNEventArgs(true, message, longMessage);
			DoRaiseScanningEvent(e);		
		}

		private void DoRaiseScanningCompleteEvent(string message, uint numberOfFilesFound) {
			MNEventArgs e = new MNEventArgs(true, message, numberOfFilesFound);
			DoRaiseScanningEvent(e);		
		}		
		
		private void DoRaiseCopyEvent(MNEventArgs e) {
			if(RaiseCopyEvent != null)
				RaiseCopyEvent(e);			
		}
		
		private void DoRaiseCopyEvent(string message, int file) {
			MNEventArgs e = new MNEventArgs(message, file);
			DoRaiseCopyEvent(e);
		}
		
		private void DoRaiseCopyingCompleteEvent(string message) {
			MNEventArgs e = new MNEventArgs(true, message, string.Empty);
			DoRaiseCopyEvent(e);				
		}
		
		private void DoRaiseCopyingCompleteEvent(string message, string longMessage) {
			MNEventArgs e = new MNEventArgs(true, message, longMessage);
			DoRaiseCopyEvent(e);		
		}
		
		private void DoRaiseSaveLoadEvent(MNEventArgs e) {
			if(RaiseSaveLoadEvent != null)
				RaiseSaveLoadEvent(e);
		}
		
		private void DoRaiseSaveLoadComplete(string message) {
			MNEventArgs e = new MNEventArgs(message);
			e.Complete = true;
			DoRaiseSaveLoadEvent(e);
		}
		
		private void DoRaiseSaveLoadEvent(string message) {
			MNEventArgs e = new MNEventArgs(message);
			DoRaiseSaveLoadEvent(e);
		}
	
		#endregion
		
        #region Scanning
		
        public bool Scan() {
			_cancelAction = false;
			
            if(Directory.Exists(_location.Path)) {				
				if(Directory.Exists(_location.PortablePath)) {
					// start scanning in the background
					BackgroundWorker scanner = new BackgroundWorker();
					scanner.DoWork += new DoWorkEventHandler(ScanDirsThread);
					scanner.RunWorkerAsync();	
				
                	return true;					
				} else {
					//portable path is not accessable
					DoRaiseScanningCompleteEvent(@"Error", string.Format(@"Path to portable device '{0}' is not accessable.", _location.Path));
					return false;
				}
			} else {
				//location path is not accessable
				DoRaiseScanningCompleteEvent(@"Error", string.Format(@"Path '{0}' is not accessable.", _location.Path));
                return false;				
			}
        }
		
		private void ScanDirsThread(object sender, DoWorkEventArgs e) {
			
			//set the datetime of this scan
			_scanningDateTime = DateTime.Now;
			
			uint numberOfFSObjectsFound = 0;
			
			// current location
			try {
				numberOfFSObjectsFound = ScanDirectory(_data.Root, _location.Path, _location.CurrentLocation, 0);
			} catch (Exception ex){
				Console.WriteLine(string.Format(@"ScanDirsThreaded cancelled at source, {0}", _data.Root.Name));
				Console.WriteLine(ex.ToString());
				_cancelAction = true;
			}
			
            // portable path
			try {
            	numberOfFSObjectsFound += ScanDirectory(_data.Root, _location.PortablePath, MNFSObjectLocation.Portable, 0);
			} catch {
				Console.WriteLine(string.Format(@"ScanDirsThreaded cancelled at portable, {0}", _data.Root.Name));
				_cancelAction = true;
			}	
			
			//only save if something was found.
			if(numberOfFSObjectsFound > 0) {
				_data.SetLastScanForLocation(_scanningDateTime, CurrentLocation);
				_hasChanged = true;	
				this.Save();
				Enqueue();
			}
			
			if(!_cancelAction) {
				DoRaiseScanningCompleteEvent(@"Successful", numberOfFSObjectsFound);
			} else {
				DoRaiseScanningCompleteEvent(@"Cancelled", @"Scanning was cancelled");
			}
		}
		
        private uint ScanDirectory(MNSynchronisedDirectory syncDirectory, string rootPath, MNFSObjectLocation location, int level) {
			uint numberOfFSObjectsFound = 0;
			if(!_cancelAction && !syncDirectory.Ignore)	{
				if(syncDirectory.Name != string.Empty && level <= kScanLevelToDisplay)
					DoRaiseScanningEvent(syncDirectory.RootlessPath);
				
				DirectoryInfo dirInfo = null;
				
				if (Directory.Exists(syncDirectory.GetFullPathWithRootPath(rootPath))) {
					dirInfo = new DirectoryInfo(syncDirectory.GetFullPathWithRootPath(rootPath));
				} else {
					//directory doesn't exist at this location, not sure how this could actually occur!
					syncDirectory.SetExistsAtForLocation(location, false);
					Console.WriteLine(string.Format(@"directory not found: {0}, {1} ", rootPath, syncDirectory.Name));
					return 0; 
				}
				
				// scan files
	            try {
					//Set exists at location for all files to false. Then deleted files can be flagged
					syncDirectory.FalsifyFilesExistAtForLocation(location);
	                //scan for files
	                foreach (FileInfo aFile in dirInfo.GetFiles()) {
						bool newFileFound = false;
	                    syncDirectory.SetChildFile(aFile, rootPath, location, _scanningDateTime, out newFileFound);
						if(newFileFound) numberOfFSObjectsFound++;						
	                }
					_hasChanged = _hasChanged | syncDirectory.RemoveUnnecessaryFiles(); //remove files that no longer exist on disk from the tree.
					
	            } catch (UnauthorizedAccessException uae) { 
	                // TODO: access exception
	                Console.WriteLine(uae.Message);
	            } catch (Exception ex) {
					Console.WriteLine(ex.Message);
				}
	
				// scan directories
	            try {
					syncDirectory.FalsifyDirectoriesExistAtForLocation(location);
	                //scan for directories
	                foreach (DirectoryInfo aDir in dirInfo.GetDirectories()) {
						MNSynchronisedDirectory newDir = null;
						bool newDirectoryFound = false;
						try	{
	                    	newDir = syncDirectory.SetChildDirectory(aDir, rootPath, location, _scanningDateTime, out newDirectoryFound);
							if(newDirectoryFound) numberOfFSObjectsFound++;
						} catch (Exception ex) {
							Console.WriteLine(ex.ToString());
						}
						
						//scan the child directory
						if (newDir != null) { 
							numberOfFSObjectsFound += ScanDirectory(newDir, rootPath, location, level+1); 
						}
	                }
					_hasChanged = _hasChanged | syncDirectory.RemoveUnnecessaryDirectories();
	            } catch (UnauthorizedAccessException uae) { 
	                // TODO: access exception
	                Console.WriteLine(uae.Message);
	            }
			} else if(syncDirectory.Ignore) {
				Console.WriteLine(string.Format(@"scanDirectory ignored {0}", syncDirectory.Name));
				_hasChanged = _hasChanged | syncDirectory.RemoveUnnecessaryDirectories() | syncDirectory.RemoveUnnecessaryFiles();				
			} else {
				Console.WriteLine(string.Format(@"scanDirectory cancelled at, {0}", syncDirectory.Name));
			}
			
			return numberOfFSObjectsFound;
        }

        #endregion
		
		#region Queue
		
		public bool Enqueue() {
			return Queue.EnqueueSynchronisedDirectory(_data.Root, this.CurrentLocation, this.ExtensionsToIgnore);			
		}
		
		#endregion
    
        #region Copying

     	/// <summary>
     	///	Copy queued files that can be copied at this location. 
     	/// </summary>
		public bool CopyQueue() {
			//TODO: threaded?
			// start scanning in the background
			BackgroundWorker scanner = new BackgroundWorker();
			scanner.DoWork += new DoWorkEventHandler(CopyQueueThread);
			scanner.RunWorkerAsync();
			
			return true;
		}
		
		private void CopyQueueThread (object sender, DoWorkEventArgs e)
		{
			string fromPathname = string.Empty;
			string toPath = string.Empty;
			string toPathname = string.Empty;
			int fileNumber = -1;
			// the nth file being copied
			
			foreach (MNQueuedFile qFile in this.Queue) {
				if (!_cancelAction && qFile.Status != MNQueuedFileStatus.Copied) {
					fileNumber++;
					if (qFile.LocationTo == MNFSObjectLocation.Portable) {
						fromPathname = Path.Combine (_location.Path, qFile.RootlessPathname);
						toPath = Path.Combine (_location.PortablePath, qFile.RootlessPath);
						toPathname = Path.Combine (toPath, qFile.Name);
					} else {
						fromPathname = Path.Combine (_location.PortablePath, qFile.RootlessPathname);
						toPath = Path.Combine (_location.Path, qFile.RootlessPath);
						toPathname = Path.Combine (toPath, qFile.Name);
					}
					
					//check to see if the source file exists
					if (System.IO.File.Exists (fromPathname)) {
						//check to see if the destination path exists
						if (!System.IO.Directory.Exists (toPath)) {
							try {
								System.IO.Directory.CreateDirectory (toPath);
								//create destination path.
								if(qFile.LocationTo == MNFSObjectLocation.Source) {
									RemoveUserIDFromSecurity(toPath);
								}								
							}
							catch (Exception ex) {
								Console.WriteLine (ex.ToString ());
								qFile.Status = MNQueuedFileStatus.Failed;
								//TODO: add message why it failed.
							}
						}
						
						//start copying the file.
						qFile.Status = MNQueuedFileStatus.Copying;
						DoRaiseCopyEvent (new MNEventArgs (qFile.Name, fileNumber));
						
						try {
							//copy
							System.IO.File.Copy (fromPathname, toPathname, true);
							
							if (System.IO.File.Exists (toPathname)) {
								//TODO: might need to check filesize for successfull copy 		
								qFile.SetExistsAtToLocation ((ulong)(new System.IO.FileInfo (toPathname).Length));
								
								if ((qFile.LocationFrom == MNFSObjectLocation.Portable) && _location.DeleteFromPortable) {
									//delete the file from the pathable device.
									if (qFile.Extension != @".MonsterNet") {
										System.IO.File.Delete (fromPathname);
										qFile.SetRemovedFromPortable ();
										CascadeDeleteEmptyDirectories (System.IO.Path.GetDirectoryName (fromPathname));
									}
								}
								
								if(qFile.LocationTo == MNFSObjectLocation.Source) {
									RemoveUserIDFromSecurity(toPathname);
								}
								
								this.HasChanged = true;
							} else {
								//TODO: copy failed.
								qFile.Status = MNQueuedFileStatus.Failed;
							}
						}
						catch (Exception ex) {
							Console.WriteLine(ex.ToString());
							qFile.Status = MNQueuedFileStatus.Failed;
						}
					} else {
						//TODO: file doesn't exist to copy.
						// mark the file as not existing at the copy from location.						
						qFile.Status = MNQueuedFileStatus.Failed;
					}				
				}
			}
			
			DoRaiseCopyingCompleteEvent(@"Complete");				
		}
		
		private void RemoveUserIDFromSecurity(string objectName) {

			if(!MNGlobal.IsUnix() && _location.RemoveUsername && _username != string.Empty) {
				try {
					bool changed = false;
					
					FileInfo file = new FileInfo(objectName);
					System.Security.AccessControl.FileSecurity fs = file.GetAccessControl();
					System.Security.AccessControl.AuthorizationRuleCollection arc = fs.GetAccessRules(true, false, typeof(System.Security.Principal.NTAccount));
										
					foreach (AuthorizationRule item in arc) {
						if(item.IdentityReference.Value == _username) {
							changed = true;
							fs.RemoveAccessRule((item as System.Security.AccessControl.FileSystemAccessRule)); 
						}
					}
					
					if(changed) {
						file.SetAccessControl(fs);
					}
				} catch (Exception ex) {
					//bah, couldn't change permissions
					Console.WriteLine(string.Format(@"Error while trying to change permissions on {0}", objectName));
					Console.WriteLine(ex.ToString());
				}
			}
		}
		
		private void CascadeDeleteEmptyDirectories(string path) {
			
			try {
				if(path != _location.PortablePath) {
					System.IO.Directory.Delete(path);
					if(path.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) 
						path = path.Substring(0,path.Length-1);
					int lastSep = path.LastIndexOf(System.IO.Path.DirectorySeparatorChar);
					path = path.Substring(0, lastSep);
					CascadeDeleteEmptyDirectories(path);
				}
			} catch(Exception ex) {
				//directory not empty.
				Console.WriteLine(ex.ToString());
			}
		}
		
        #endregion		
	}
	

}